iT邦幫忙

2022 iThome 鐵人賽

DAY 5
2

前言

這篇文章將會介紹 Execution Context,然後也會在了解的過程中一併認識 Hoisting、Scope Chain。


Execution Context (中文翻成: 執行上下文)

Execution Context 是一種抽象的概念,在 JS 程式碼被編譯解析後建立,用來執行程式碼的環境,Execution Context 分成兩種:

1. Global Execution Context

JavaScript 預設的 Execution Context,只會有一個,裡面會建立全域物件(window)。

2. Function Execution Context

當函式呼叫時,會形成函式自己的 Execution Context。

而在一個 Execution Context 裡面,還有 Lexical Environment 和 Variable Environment,下面一起來了解它們吧!


範例說明

首先我們先寫一段程式碼:

console.log(dogName);
console.log(showAge);
console.log(showWeight);

var dogName = 'Lucky';

var showAge = function() {
  return '5 years old';
}

function showWeight() {
  return '20 kg';
}

function introMyDog(name, age, weight) {
  var introduction = `My dog's name is ${dogName}. It is ${age} and weight ${weight}.`
  return introduction;
}

var message = introMyDog(dogName, showAge(), showWeight());

console.log(message);

接著我們一步步分析這段程式碼的執行過程。

推薦讀者使用雙螢幕閱讀,一邊放程式碼一邊放說明的內容就不用上下頻繁滾動,或是把程式碼貼在瀏覽器 devTools 並配著看也可以!

1. 編譯階段完成時

編譯階段完成時,產生全域的 Execution Context,並且放在 Call Stack 裡面。

此時程式裡面宣告的變數和函式,也在 Memory Heap 裡面分配了記憶體位置,不過這時變數尚未被賦值,在記憶體為 undefined,像 dogName、showAge 等變數/函數表達式就印出 undefined,但整個函式會被存放到記憶體內,所以 showWeight、introMyDog 能印出完整函式。

從程式碼的角度看,它們就好像移動到作用域的頂端一樣,這個就是 hoisting。

2. 進入執行階段

進入執行階段時,所有變數被賦值。

3. 依序執行程式,執行 showAge 和 showWeight 函式

接著就依序執行程式碼,到達var message = introMyDog(dogName, showAge(), showWeight()); 這行程式碼時,showAge 函式和 showWeight 會先呼叫,它們回傳的結果再當成參數傳入 introMyDog。

這個階段兩個函式內部沒啥程式碼,簡單帶過。

先執行 showAge 函式,執行完後其 Execution Context 移出 Stack。

showWeight 執行完後 Execution Context 也移出 Stack。

4. 最後執行 introMyDog 函式

首先會建立 introMyDog 的 Execution Context,並且和全域 Execution Context 時一樣,分為編譯函式內的程式碼和執行兩個階段。

函式內部一樣也有 hoisting

最後就可以將 introMyDog 函式回傳的值賦予給 message 變數了,然後一樣將 introMyDog 的 Execution Context 移出 Stack。


作用域(Scope) & 作用域鏈(Scope Chain)

接著延續前面提到的程式碼範例,我們來看作用域 & 作用域鏈。

作用域指的是我們宣告的變數、常數、函式可以被存取得到的範圍,分為三種:

  • 全域作用域
  • 函式作用域
  • 區塊作用域

例如在 introMyDog 的函式作用域中可以取用到 introduction 變數,而如果在 introMyDog 函式內要取到 dogName 這個全域變數,就需要往外層的全域作用域查找,一層層往外層的作用域找直到找到為止,這種特性就叫作用域鏈。

補充: JS 屬於靜態作用域,也就是在函式定義時決定其作用域。與之相對的是動態作用域,也就是在函式執行時才決定其作用域

範例說明

以下範例重複宣告了三次 height,讀者可以思考看看最後 showHeight 函式印出的值是什麼?

var height = 170;

function showHeight() {
  console.log(height); // ?
}

function setHeight160() {
  var height = 160;
  showHeight();
}

function setHeight180() {
  var height = 180;
  setHeight160();
}

setHeight180();

讀完程式碼後,這段程式執行到呼叫 showHeight 函式時的樣子會如下圖:

在 showHeight 函式內找不到 height 變數,所以往外層作用域查找,最後印出全域 execution context 的 170。

為什麼 showHeight 找不到變數不是往 setHeight160 的 execution context 查找? 因為 showHeight 的外層作用域就是全域!
我們來看下面範例來做比較。

接著我們把範例程式碼調整成這樣:

var height = 170;

function setHeight180() {
  var height = 180;

  function setHeight160() {
    var height = 160;
    
    function showHeight() {
      console.log(height);
    }
    showHeight();
  }
  setHeight160();
}

setHeight180();

這樣的話就會印出 160,showHeight 函式找不到 height 變數,就會向外層作用域找,所以找到 setHeight160 函式的變數。

紅色箭頭代表程式碼找到 setHeight160 函式就停止,若一直找不到就會往外繼續查。


這篇介紹了 Execution Context、Hoisting、Scope 等概念,下一篇我會繼續延伸,加入 let/const、Block Scope(區塊作用域) 等概念和本篇文章的概念做整合。


上一篇
Day4-JavaScript Runtime Environment 觀念
下一篇
Day6-JavaScript Execution Context & var/let/const
系列文
強化 JavaScript 之 - 程式語感是可以磨練成就的30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言